package org.springframework.beans.support;

import org.springframework.beans.BeansException;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.*;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;
import org.springframework.beans.util.BeanUtil;
import org.springframework.beans.util.Misc;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

/**
 * AbstractAutowireCapableBeanFactory
 *
 * @author xiaohei
 */
public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory {

    private InstantiationStrategy instantiation = new CglibSubclassingInstantiationStrategy();

    @Override
    public Object createBean(String beanName, BeanDefinition beanDefinition, Object[] args) throws BeansException {
        Object bean = null;
        try {
            bean = resolveBeforeInstantiation(beanName, beanDefinition);
            if (bean != null) {
                return bean;
            }
            bean = doCreateBean(beanName, beanDefinition, args);
        } catch (Exception e) {
            throw new BeansException("failed to instance " + beanName);
        }
        registerSingleton(beanName, bean);
        return bean;
    }

    private Object doCreateBean(String beanName, BeanDefinition beanDefinition, Object[] args) {
        Object beanInstance = createBeanInstance(beanName, beanDefinition, args);
        // 初始化bean
        populateBean(beanName, beanDefinition, beanInstance);
        // 方法初始化和回调
        beanInstance = initializeBean(beanName, beanDefinition, beanInstance);
        return beanInstance;
    }

    private Object resolveBeforeInstantiation(String beanName, BeanDefinition bd) {
        Object bean = applyBeanPostProcessorBeforeInstantiation(beanName, bd.getBeanClass());
        if (bean != null) {
            bean = applyBeanPostProcessorAfterInstantiation(bean, beanName);
        }
        return bean;
    }

    private Object applyBeanPostProcessorAfterInstantiation(Object bean, String beanName) {
        Object result = bean;
        for (InstantiationAwareBeanPostProcessor beanPostProcessor : getBeanPostProcessorCache().instantiationAware) {
            Object current = beanPostProcessor.postProcessAfterInitialization(result, beanName);
            if (current == null) {
                return null;
            }
            result = current;
        }
        return bean;
    }

    private Object applyBeanPostProcessorBeforeInstantiation(String beanName, Class<?> beanClass) {
        for (InstantiationAwareBeanPostProcessor beanPostProcessor : getBeanPostProcessorCache().instantiationAware) {
            Object result = beanPostProcessor.postProcessBeforeInstantiation(beanClass, beanName);
            if (result != null) {
                return result;
            }
        }
        return null;
    }

    private void populateBean(String beanName, BeanDefinition bd, Object beanInstance) {
        if (beanInstance == null) {
            if (bd.hasPropertyValues()) {
                throw new BeansException("could not apply property values to null instance");
            }
        }

        for (InstantiationAwareBeanPostProcessor bpp : getBeanPostProcessorCache().instantiationAware) {
            if (!bpp.postProcessAfterInstantiation(beanInstance, beanName)) {
                return;
            }
        }

        PropertyValues pvs = bd.hasPropertyValues() ? bd.getPropertyValues() : null;
        for (InstantiationAwareBeanPostProcessor bpp : getBeanPostProcessorCache().instantiationAware) {
            PropertyValues pvsToUse = bpp.postProcessProperties(pvs, beanInstance, beanName);
            if (pvsToUse == null) {
                return;
            }
            pvs = pvsToUse;
        }
        // 属性注入
        if (pvs != null) {
            applyPropertyValue(pvs, beanInstance);
        }
    }

    private Object initializeBean(String beanName, BeanDefinition bd, Object bean) {
        try {
            bean = applyBeanPostProcessorBeforeInitialization(bean, beanName);
            // 初始化回调
            invokeAwareMethods(beanName, bean);
            // 初始化方法
            invokeInitMethods(beanName, bean, bd);
            // 初始化后置回调
            bean = applyBeanPostProcessorAfterInitialization(bean, beanName);
            // beanGC
            registerDisposableBeanIfNecessary(beanName, new DisposableBeanAdapter(beanName, bean, bd.getDestroyMethodName()));
            return bean;
        } catch (Exception e) {
            throw new BeansException("initialize bean " + beanName + "failed");
        }
    }

    private Object applyBeanPostProcessorBeforeInitialization(Object bean, String beanName) {
        Object current = bean;
        for (InstantiationAwareBeanPostProcessor bpp : getBeanPostProcessorCache().instantiationAware) {
            Object result = bpp.postProcessBeforeInitialization(bean, beanName);
            if (current == null) {
                return result;
            }
            result = current;
        }
        return current;
    }

    private void registerDisposableBeanIfNecessary(String beanName, DisposableBeanAdapter disposableBeanAdapter) {
        registerDisposableBean(beanName, disposableBeanAdapter);
    }

    private Object applyBeanPostProcessorAfterInitialization(Object bean, String beanName) {
        Object result = bean;
        for (InstantiationAwareBeanPostProcessor bpp : getBeanPostProcessorCache().instantiationAware) {
            Object current = bpp.postProcessAfterInitialization(bean, beanName);
            if (current == null) {
                return result;
            }
            result = current;
        }
        return result;
    }

    private void invokeAwareMethods(String beanName, Object bean) {
        if (bean instanceof Aware) {
            if (bean instanceof BeanNameAware) {
                ((BeanNameAware) bean).setBeanName(beanName);
            }
            if (bean instanceof ClassLoaderAware) {
                ClassLoader cl = this.getClassLoader();
                if (cl != null) {
                    ((ClassLoaderAware) bean).setClassLoader(cl);
                }
            }
            if (bean instanceof BeanFactoryAware) {
                ((BeanFactoryAware) bean).setBeanFactory(this);
            }
        }
    }

    private void invokeInitMethods(String beanName, Object bean, BeanDefinition bd) throws Exception {
        if (bean instanceof InitializingBean) {
            ((InitializingBean) bean).afterPropertiesSet();
        }
        if (Misc.isNotEmpty(bd.getInitMethodName())) {
            Method initMethod = bd.getBeanClass().getDeclaredMethod(bd.getInitMethodName());
            if (initMethod == null) {
                throw new BeansException("could not find init method " + bd.getInitMethodName());
            }
            initMethod.invoke(bean, null);
        }
    }


    private void applyPropertyValue(PropertyValues pvs, Object beanInstance) {
        if (pvs == null) {
            return;
        }
        for (PropertyValue pv : pvs.getPropertyValues()) {
            String name = pv.getName();
            String value = pv.getValue();
            BeanUtil.setFieldValue(beanInstance, name, value);
        }
    }

    private Object createBeanInstance(String beanName, BeanDefinition beanDefinition, Object[] args) {
        Constructor ctorToUse = null;
        if (args != null) {
            Constructor<?>[] declaredConstructors = beanDefinition.getBeanClass().getDeclaredConstructors();
            for (Constructor<?> ctor : declaredConstructors) {
                if (ctor.getParameterTypes().length == args.length) {
                    ctorToUse = ctor;
                    break;
                }
            }
        }
        return getInstantiationStrategy().instantiate(beanName, beanDefinition, ctorToUse, args);
    }

    public void setInstantiation(InstantiationStrategy instantiation) {
        this.instantiation = instantiation;
    }

    protected InstantiationStrategy getInstantiationStrategy() {
        return this.instantiation;
    }
}
